1   /*
2    * Copyright (c) 2003, 2006, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package com.sun.security.sasl.digest;
27  
28  import java.security.AccessController;
29  import java.security.Provider;
30  import java.security.MessageDigest;
31  import java.security.NoSuchAlgorithmException;
32  import java.io.ByteArrayOutputStream;
33  import java.io.ByteArrayInputStream;
34  import java.io.IOException;
35  import java.io.UnsupportedEncodingException;
36  import java.util.Random;
37  import java.util.StringTokenizer;
38  import java.util.ArrayList;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Set;
42  import java.util.Arrays;
43  
44  import java.util.logging.Logger;
45  import java.util.logging.Level;
46  
47  import javax.security.sasl.*;
48  import javax.security.auth.callback.*;
49  
50  /**
51    * An implementation of the DIGEST-MD5 server SASL mechanism.
52    * (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>)
53    * <p>
54    * The DIGEST-MD5 SASL mechanism specifies two modes of authentication.
55    * <ul><li>Initial Authentication
56    * <li>Subsequent Authentication - optional, (currently not supported)
57    * </ul>
58    *
59    * Required callbacks:
60    * - RealmCallback
61    *      used as key by handler to fetch password
62    * - NameCallback
63    *      used as key by handler to fetch password
64    * - PasswordCallback
65    *      handler must enter password for username/realm supplied
66    * - AuthorizeCallback
67    *      handler must verify that authid/authzids are allowed and set
68    *      authorized ID to be the canonicalized authzid (if applicable).
69    *
70    * Environment properties that affect the implementation:
71    * javax.security.sasl.qop:
72    *    specifies list of qops; default is "auth"; typically, caller should set
73    *    this to "auth, auth-int, auth-conf".
74    * javax.security.sasl.strength
75    *    specifies low/medium/high strength of encryption; default is all available
76    *    ciphers [high,medium,low]; high means des3 or rc4 (128); medium des or
77    *    rc4-56; low is rc4-40.
78    * javax.security.sasl.maxbuf
79    *    specifies max receive buf size; default is 65536
80    * javax.security.sasl.sendmaxbuffer
81    *    specifies max send buf size; default is 65536 (min of this and client's max
82    *    recv size)
83    *
84    * com.sun.security.sasl.digest.utf8:
85    *    "true" means to use UTF-8 charset; "false" to use ISO-8859-1 encoding;
86    *    default is "true".
87    * com.sun.security.sasl.digest.realm:
88    *    space-separated list of realms; default is server name (fqdn parameter)
89    *
90    * @author Rosanna Lee
91    */
92  
93  final class DigestMD5Server extends DigestMD5Base implements SaslServer {
94      private static final String MY_CLASS_NAME = DigestMD5Server.class.getName();
95  
96      private static final String UTF8_DIRECTIVE = "charset=utf-8,";
97      private static final String ALGORITHM_DIRECTIVE = "algorithm=md5-sess";
98  
99      /*
100      * Always expect nonce count value to be 1 because we support only
101      * initial authentication.
102      */
103     private static final int NONCE_COUNT_VALUE = 1;
104 
105     /* "true" means use UTF8; "false" ISO 8859-1; default is "true" */
106     private static final String UTF8_PROPERTY =
107         "com.sun.security.sasl.digest.utf8";
108 
109     /* List of space-separated realms used for authentication */
110     private static final String REALM_PROPERTY =
111         "com.sun.security.sasl.digest.realm";
112 
113     /* Directives encountered in responses sent by the client. */
114     private static final String[] DIRECTIVE_KEY = {
115         "username",    // exactly once
116         "realm",       // exactly once if sent by server
117         "nonce",       // exactly once
118         "cnonce",      // exactly once
119         "nonce-count", // atmost once; default is 00000001
120         "qop",         // atmost once; default is "auth"
121         "digest-uri",  // atmost once; (default?)
122         "response",    // exactly once
123         "maxbuf",      // atmost once; default is 65536
124         "charset",     // atmost once; default is ISO-8859-1
125         "cipher",      // exactly once if qop is "auth-conf"
126         "authzid",     // atmost once; default is none
127         "auth-param",  // >= 0 times (ignored)
128     };
129 
130     /* Indices into DIRECTIVE_KEY */
131     private static final int USERNAME = 0;
132     private static final int REALM = 1;
133     private static final int NONCE = 2;
134     private static final int CNONCE = 3;
135     private static final int NONCE_COUNT = 4;
136     private static final int QOP = 5;
137     private static final int DIGEST_URI = 6;
138     private static final int RESPONSE = 7;
139     private static final int MAXBUF = 8;
140     private static final int CHARSET = 9;
141     private static final int CIPHER = 10;
142     private static final int AUTHZID = 11;
143     private static final int AUTH_PARAM = 12;
144 
145     /* Server-generated/supplied information */
146     private String specifiedQops;
147     private byte[] myCiphers;
148     private List<String> serverRealms;
149 
150     DigestMD5Server(String protocol, String serverName, Map props,
151         CallbackHandler cbh) throws SaslException {
152         super(props, MY_CLASS_NAME, 1, protocol + "/" + serverName, cbh);
153 
154         serverRealms = new ArrayList<String>();
155 
156         useUTF8 = true;  // default
157 
158         if (props != null) {
159             specifiedQops = (String) props.get(Sasl.QOP);
160             if ("false".equals((String) props.get(UTF8_PROPERTY))) {
161                 useUTF8 = false;
162                 logger.log(Level.FINE, "DIGEST80:Server supports ISO-Latin-1");
163             }
164 
165             String realms = (String) props.get(REALM_PROPERTY);
166             if (realms != null) {
167                 StringTokenizer parser = new StringTokenizer(realms, ", \t\n");
168                 int tokenCount = parser.countTokens();
169                 String token = null;
170                 for (int i = 0; i < tokenCount; i++) {
171                     token = parser.nextToken();
172                     logger.log(Level.FINE, "DIGEST81:Server supports realm {0}",
173                         token);
174                     serverRealms.add(token);
175                 }
176             }
177         }
178 
179         encoding = (useUTF8 ? "UTF8" : "8859_1");
180 
181         // By default, use server name as realm
182         if (serverRealms.size() == 0) {
183             serverRealms.add(serverName);
184         }
185     }
186 
187     public  byte[] evaluateResponse(byte[] response) throws SaslException {
188         if (response.length > MAX_RESPONSE_LENGTH) {
189             throw new SaslException(
190                 "DIGEST-MD5: Invalid digest response length. Got:  " +
191                 response.length + " Expected < " + MAX_RESPONSE_LENGTH);
192         }
193 
194         byte[] challenge;
195         switch (step) {
196         case 1:
197             if (response.length != 0) {
198                 throw new SaslException(
199                     "DIGEST-MD5 must not have an initial response");
200             }
201 
202             /* Generate first challenge */
203             String supportedCiphers = null;
204             if ((allQop&PRIVACY_PROTECTION) != 0) {
205                 myCiphers = getPlatformCiphers();
206                 StringBuffer buf = new StringBuffer();
207 
208                 // myCipher[i] is a byte that indicates whether CIPHER_TOKENS[i]
209                 // is supported
210                 for (int i = 0; i < CIPHER_TOKENS.length; i++) {
211                     if (myCiphers[i] != 0) {
212                         if (buf.length() > 0) {
213                             buf.append(',');
214                         }
215                         buf.append(CIPHER_TOKENS[i]);
216                     }
217                 }
218                 supportedCiphers = buf.toString();
219             }
220 
221             try {
222                 challenge = generateChallenge(serverRealms, specifiedQops,
223                     supportedCiphers);
224 
225                 step = 3;
226                 return challenge;
227             } catch (UnsupportedEncodingException e) {
228                 throw new SaslException(
229                     "DIGEST-MD5: Error encoding challenge", e);
230             } catch (IOException e) {
231                 throw new SaslException(
232                     "DIGEST-MD5: Error generating challenge", e);
233             }
234 
235             // Step 2 is performed by client
236 
237         case 3:
238             /* Validates client's response and generate challenge:
239              *    response-auth = "rspauth" "=" response-value
240              */
241             try {
242                 byte[][] responseVal = parseDirectives(response, DIRECTIVE_KEY,
243                     null, REALM);
244                 challenge = validateClientResponse(responseVal);
245             } catch (SaslException e) {
246                 throw e;
247             } catch (UnsupportedEncodingException e) {
248                 throw new SaslException(
249                     "DIGEST-MD5: Error validating client response", e);
250             } finally {
251                 step = 0;  // Set to invalid state
252             }
253 
254             completed = true;
255 
256             /* Initialize SecurityCtx implementation */
257             if (integrity && privacy) {
258                 secCtx = new DigestPrivacy(false /* not client */);
259             } else if (integrity) {
260                 secCtx = new DigestIntegrity(false /* not client */);
261             }
262 
263             return challenge;
264 
265         default:
266             // No other possible state
267             throw new SaslException("DIGEST-MD5: Server at illegal state");
268         }
269     }
270 
271     /**
272      * Generates challenge to be sent to client.
273      *  digest-challenge  =
274      *    1#( realm | nonce | qop-options | stale | maxbuf | charset
275      *               algorithm | cipher-opts | auth-param )
276      *
277      *        realm             = "realm" "=" <"> realm-value <">
278      *        realm-value       = qdstr-val
279      *        nonce             = "nonce" "=" <"> nonce-value <">
280      *        nonce-value       = qdstr-val
281      *        qop-options       = "qop" "=" <"> qop-list <">
282      *        qop-list          = 1#qop-value
283      *        qop-value         = "auth" | "auth-int" | "auth-conf" |
284      *                             token
285      *        stale             = "stale" "=" "true"
286      *        maxbuf            = "maxbuf" "=" maxbuf-value
287      *        maxbuf-value      = 1*DIGIT
288      *        charset           = "charset" "=" "utf-8"
289      *        algorithm         = "algorithm" "=" "md5-sess"
290      *        cipher-opts       = "cipher" "=" <"> 1#cipher-value <">
291      *        cipher-value      = "3des" | "des" | "rc4-40" | "rc4" |
292      *                            "rc4-56" | token
293      *        auth-param        = token "=" ( token | quoted-string )
294      */
295     private byte[] generateChallenge(List<String> realms, String qopStr,
296         String cipherStr) throws UnsupportedEncodingException, IOException {
297         ByteArrayOutputStream out = new ByteArrayOutputStream();
298 
299         // Realms (>= 0)
300         for (int i = 0; realms != null && i < realms.size(); i++) {
301             out.write("realm=\"".getBytes(encoding));
302             writeQuotedStringValue(out, realms.get(i).getBytes(encoding));
303             out.write('"');
304             out.write(',');
305         }
306 
307         // Nonce - required (1)
308         out.write(("nonce=\"").getBytes(encoding));
309         nonce = generateNonce();
310         writeQuotedStringValue(out, nonce);
311         out.write('"');
312         out.write(',');
313 
314         // QOP - optional (1) [default: auth]
315         // qop="auth,auth-conf,auth-int"
316         if (qopStr != null) {
317             out.write(("qop=\"").getBytes(encoding));
318             // Check for quotes in case of non-standard qop options
319             writeQuotedStringValue(out, qopStr.getBytes(encoding));
320             out.write('"');
321             out.write(',');
322         }
323 
324         // maxbuf - optional (1) [default: 65536]
325         if (recvMaxBufSize != DEFAULT_MAXBUF) {
326             out.write(("maxbuf=\"" + recvMaxBufSize + "\",").getBytes(encoding));
327         }
328 
329         // charset - optional (1) [default: ISO 8859_1]
330         if (useUTF8) {
331             out.write(UTF8_DIRECTIVE.getBytes(encoding));
332         }
333 
334         if (cipherStr != null) {
335             out.write("cipher=\"".getBytes(encoding));
336             // Check for quotes in case of custom ciphers
337             writeQuotedStringValue(out, cipherStr.getBytes(encoding));
338             out.write('"');
339             out.write(',');
340         }
341 
342         // algorithm - required (1)
343         out.write(ALGORITHM_DIRECTIVE.getBytes(encoding));
344 
345         return out.toByteArray();
346     }
347 
348     /**
349      * Validates client's response.
350      *   digest-response  = 1#( username | realm | nonce | cnonce |
351      *                          nonce-count | qop | digest-uri | response |
352      *                          maxbuf | charset | cipher | authzid |
353      *                          auth-param )
354      *
355      *       username         = "username" "=" <"> username-value <">
356      *       username-value   = qdstr-val
357      *       cnonce           = "cnonce" "=" <"> cnonce-value <">
358      *       cnonce-value     = qdstr-val
359      *       nonce-count      = "nc" "=" nc-value
360      *       nc-value         = 8LHEX
361      *       qop              = "qop" "=" qop-value
362      *       digest-uri       = "digest-uri" "=" <"> digest-uri-value <">
363      *       digest-uri-value  = serv-type "/" host [ "/" serv-name ]
364      *       serv-type        = 1*ALPHA
365      *       host             = 1*( ALPHA | DIGIT | "-" | "." )
366      *       serv-name        = host
367      *       response         = "response" "=" response-value
368      *       response-value   = 32LHEX
369      *       LHEX             = "0" | "1" | "2" | "3" |
370      *                          "4" | "5" | "6" | "7" |
371      *                          "8" | "9" | "a" | "b" |
372      *                          "c" | "d" | "e" | "f"
373      *       cipher           = "cipher" "=" cipher-value
374      *       authzid          = "authzid" "=" <"> authzid-value <">
375      *       authzid-value    = qdstr-val
376      * sets:
377      *   negotiatedQop
378      *   negotiatedCipher
379      *   negotiatedRealm
380      *   negotiatedStrength
381      *   digestUri (checked and set to clients to account for case diffs)
382      *   sendMaxBufSize
383      *   authzid (gotten from callback)
384      * @return response-value ('rspauth') for client to validate
385      */
386     private byte[] validateClientResponse(byte[][] responseVal)
387         throws SaslException, UnsupportedEncodingException {
388 
389         /* CHARSET: optional atmost once */
390         if (responseVal[CHARSET] != null) {
391             // The client should send this directive only if the server has
392             // indicated it supports UTF-8.
393             if (!useUTF8 ||
394                 !"utf-8".equals(new String(responseVal[CHARSET], encoding))) {
395                 throw new SaslException("DIGEST-MD5: digest response format " +
396                     "violation. Incompatible charset value: " +
397                     new String(responseVal[CHARSET]));
398             }
399         }
400 
401         // maxbuf: atmost once
402         int clntMaxBufSize =
403             (responseVal[MAXBUF] == null) ? DEFAULT_MAXBUF
404             : Integer.parseInt(new String(responseVal[MAXBUF], encoding));
405 
406         // Max send buf size is min of client's max recv buf size and
407         // server's max send buf size
408         sendMaxBufSize = ((sendMaxBufSize == 0) ? clntMaxBufSize :
409             Math.min(sendMaxBufSize, clntMaxBufSize));
410 
411         /* username: exactly once */
412         String username;
413         if (responseVal[USERNAME] != null) {
414             username = new String(responseVal[USERNAME], encoding);
415             logger.log(Level.FINE, "DIGEST82:Username: {0}", username);
416         } else {
417             throw new SaslException("DIGEST-MD5: digest response format " +
418                 "violation. Missing username.");
419         }
420 
421         /* realm: exactly once if sent by server */
422         negotiatedRealm = ((responseVal[REALM] != null) ?
423             new String(responseVal[REALM], encoding) : "");
424         logger.log(Level.FINE, "DIGEST83:Client negotiated realm: {0}",
425             negotiatedRealm);
426 
427         if (!serverRealms.contains(negotiatedRealm)) {
428             // Server had sent at least one realm
429             // Check that response is one of these
430             throw new SaslException("DIGEST-MD5: digest response format " +
431                 "violation. Nonexistent realm: " + negotiatedRealm);
432         }
433         // Else, client specified realm was one of server's or server had none
434 
435         /* nonce: exactly once */
436         if (responseVal[NONCE] == null) {
437             throw new SaslException("DIGEST-MD5: digest response format " +
438                 "violation. Missing nonce.");
439         }
440         byte[] nonceFromClient = responseVal[NONCE];
441         if (!Arrays.equals(nonceFromClient, nonce)) {
442             throw new SaslException("DIGEST-MD5: digest response format " +
443                 "violation. Mismatched nonce.");
444         }
445 
446         /* cnonce: exactly once */
447         if (responseVal[CNONCE] == null) {
448             throw new SaslException("DIGEST-MD5: digest response format " +
449                 "violation. Missing cnonce.");
450         }
451         byte[] cnonce = responseVal[CNONCE];
452 
453         /* nonce-count: atmost once */
454         if (responseVal[NONCE_COUNT] != null &&
455             NONCE_COUNT_VALUE != Integer.parseInt(
456                 new String(responseVal[NONCE_COUNT], encoding), 16)) {
457             throw new SaslException("DIGEST-MD5: digest response format " +
458                 "violation. Nonce count does not match: " +
459                 new String(responseVal[NONCE_COUNT]));
460         }
461 
462         /* qop: atmost once; default is "auth" */
463         negotiatedQop = ((responseVal[QOP] != null) ?
464             new String(responseVal[QOP], encoding) : "auth");
465 
466         logger.log(Level.FINE, "DIGEST84:Client negotiated qop: {0}",
467             negotiatedQop);
468 
469         // Check that QOP is one sent by server
470         byte cQop;
471         if (negotiatedQop.equals("auth")) {
472             cQop = NO_PROTECTION;
473         } else if (negotiatedQop.equals("auth-int")) {
474             cQop = INTEGRITY_ONLY_PROTECTION;
475             integrity = true;
476             rawSendSize = sendMaxBufSize - 16;
477         } else if (negotiatedQop.equals("auth-conf")) {
478             cQop = PRIVACY_PROTECTION;
479             integrity = privacy = true;
480             rawSendSize = sendMaxBufSize - 26;
481         } else {
482             throw new SaslException("DIGEST-MD5: digest response format " +
483                 "violation. Invalid QOP: " + negotiatedQop);
484         }
485         if ((cQop&allQop) == 0) {
486             throw new SaslException("DIGEST-MD5: server does not support " +
487                 " qop: " + negotiatedQop);
488         }
489 
490         if (privacy) {
491             negotiatedCipher = ((responseVal[CIPHER] != null) ?
492                 new String(responseVal[CIPHER], encoding) : null);
493             if (negotiatedCipher == null) {
494                 throw new SaslException("DIGEST-MD5: digest response format " +
495                     "violation. No cipher specified.");
496             }
497 
498             int foundCipher = -1;
499             logger.log(Level.FINE, "DIGEST85:Client negotiated cipher: {0}",
500                 negotiatedCipher);
501 
502             // Check that cipher is one that we offered
503             for (int j = 0; j < CIPHER_TOKENS.length; j++) {
504                 if (negotiatedCipher.equals(CIPHER_TOKENS[j]) &&
505                     myCiphers[j] != 0) {
506                     foundCipher = j;
507                     break;
508                 }
509             }
510             if (foundCipher == -1) {
511                 throw new SaslException("DIGEST-MD5: server does not " +
512                     "support cipher: " + negotiatedCipher);
513             }
514             // Set negotiatedStrength
515             if ((CIPHER_MASKS[foundCipher]&HIGH_STRENGTH) != 0) {
516                 negotiatedStrength = "high";
517             } else if ((CIPHER_MASKS[foundCipher]&MEDIUM_STRENGTH) != 0) {
518                 negotiatedStrength = "medium";
519             } else {
520                 // assume default low
521                 negotiatedStrength = "low";
522             }
523 
524             logger.log(Level.FINE, "DIGEST86:Negotiated strength: {0}",
525                 negotiatedStrength);
526         }
527 
528         // atmost once
529         String digestUriFromResponse = ((responseVal[DIGEST_URI]) != null ?
530             new String(responseVal[DIGEST_URI], encoding) : null);
531 
532         if (digestUriFromResponse != null) {
533             logger.log(Level.FINE, "DIGEST87:digest URI: {0}",
534                 digestUriFromResponse);
535         }
536 
537         // serv-type "/" host [ "/" serv-name ]
538         // e.g.: smtp/mail3.example.com/example.com
539         // e.g.: ftp/ftp.example.com
540         // e.g.: ldap/ldapserver.example.com
541 
542         // host should match one of service's configured service names
543         // Check against digest URI that mech was created with
544 
545         if (digestUri.equalsIgnoreCase(digestUriFromResponse)) {
546             digestUri = digestUriFromResponse; // account for case-sensitive diffs
547         } else {
548             throw new SaslException("DIGEST-MD5: digest response format " +
549                 "violation. Mismatched URI: " + digestUriFromResponse +
550                 "; expecting: " + digestUri);
551         }
552 
553         // response: exactly once
554         byte[] responseFromClient = responseVal[RESPONSE];
555         if (responseFromClient == null) {
556             throw new SaslException("DIGEST-MD5: digest response format " +
557                 " violation. Missing response.");
558         }
559 
560         // authzid: atmost once
561         byte[] authzidBytes;
562         String authzidFromClient = ((authzidBytes=responseVal[AUTHZID]) != null?
563             new String(authzidBytes, encoding) : username);
564 
565         if (authzidBytes != null) {
566             logger.log(Level.FINE, "DIGEST88:Authzid: {0}",
567                 new String(authzidBytes));
568         }
569 
570         // Ignore auth-param
571 
572         // Get password need to generate verifying response
573         char[] passwd;
574         try {
575             // Realm and Name callbacks are used to provide info
576             RealmCallback rcb = new RealmCallback("DIGEST-MD5 realm: ",
577                 negotiatedRealm);
578             NameCallback ncb = new NameCallback("DIGEST-MD5 authentication ID: ",
579                 username);
580 
581             // PasswordCallback is used to collect info
582             PasswordCallback pcb =
583                 new PasswordCallback("DIGEST-MD5 password: ", false);
584 
585             cbh.handle(new Callback[] {rcb, ncb, pcb});
586             passwd = pcb.getPassword();
587             pcb.clearPassword();
588 
589         } catch (UnsupportedCallbackException e) {
590             throw new SaslException(
591                 "DIGEST-MD5: Cannot perform callback to acquire password", e);
592 
593         } catch (IOException e) {
594             throw new SaslException(
595                 "DIGEST-MD5: IO error acquiring password", e);
596         }
597 
598         if (passwd == null) {
599             throw new SaslException(
600                 "DIGEST-MD5: cannot acquire password for " + username +
601                 " in realm : " + negotiatedRealm);
602         }
603 
604         try {
605             // Validate response value sent by client
606             byte[] expectedResponse;
607 
608             try {
609                 expectedResponse = generateResponseValue("AUTHENTICATE",
610                     digestUri, negotiatedQop, username, negotiatedRealm,
611                     passwd, nonce /* use own nonce */,
612                     cnonce, NONCE_COUNT_VALUE, authzidBytes);
613 
614             } catch (NoSuchAlgorithmException e) {
615                 throw new SaslException(
616                     "DIGEST-MD5: problem duplicating client response", e);
617             } catch (IOException e) {
618                 throw new SaslException(
619                     "DIGEST-MD5: problem duplicating client response", e);
620             }
621 
622             if (!Arrays.equals(responseFromClient, expectedResponse)) {
623                 throw new SaslException("DIGEST-MD5: digest response format " +
624                     "violation. Mismatched response.");
625             }
626 
627             // Ensure that authzid mapping is OK
628             try {
629                 AuthorizeCallback acb =
630                     new AuthorizeCallback(username, authzidFromClient);
631                 cbh.handle(new Callback[]{acb});
632 
633                 if (acb.isAuthorized()) {
634                     authzid = acb.getAuthorizedID();
635                 } else {
636                     throw new SaslException("DIGEST-MD5: " + username +
637                         " is not authorized to act as " + authzidFromClient);
638                 }
639             } catch (SaslException e) {
640                 throw e;
641             } catch (UnsupportedCallbackException e) {
642                 throw new SaslException(
643                     "DIGEST-MD5: Cannot perform callback to check authzid", e);
644             } catch (IOException e) {
645                 throw new SaslException(
646                     "DIGEST-MD5: IO error checking authzid", e);
647             }
648 
649             return generateResponseAuth(username, passwd, cnonce,
650                 NONCE_COUNT_VALUE, authzidBytes);
651         } finally {
652             // Clear password
653             for (int i = 0; i < passwd.length; i++) {
654                 passwd[i] = 0;
655             }
656         }
657     }
658 
659     /**
660      * Server sends a message formatted as follows:
661      *    response-auth = "rspauth" "=" response-value
662      *   where response-value is calculated as above, using the values sent in
663      *   step two, except that if qop is "auth", then A2 is
664      *
665      *       A2 = { ":", digest-uri-value }
666      *
667      *   And if qop is "auth-int" or "auth-conf" then A2 is
668      *
669      *       A2 = { ":", digest-uri-value, ":00000000000000000000000000000000" }
670      *
671      * Clears password afterwards.
672      */
673     private byte[] generateResponseAuth(String username, char[] passwd,
674         byte[] cnonce, int nonceCount, byte[] authzidBytes) throws SaslException {
675 
676         // Construct response value
677 
678         try {
679             byte[] responseValue = generateResponseValue("",
680                 digestUri, negotiatedQop, username, negotiatedRealm,
681                 passwd, nonce, cnonce, nonceCount, authzidBytes);
682 
683             byte[] challenge = new byte[responseValue.length + 8];
684             System.arraycopy("rspauth=".getBytes(encoding), 0, challenge, 0, 8);
685             System.arraycopy(responseValue, 0, challenge, 8,
686                 responseValue.length );
687 
688             return challenge;
689 
690         } catch (NoSuchAlgorithmException e) {
691             throw new SaslException("DIGEST-MD5: problem generating response", e);
692         } catch (IOException e) {
693             throw new SaslException("DIGEST-MD5: problem generating response", e);
694         }
695     }
696 
697     public String getAuthorizationID() {
698         if (completed) {
699             return authzid;
700         } else {
701             throw new IllegalStateException(
702                 "DIGEST-MD5 server negotiation not complete");
703         }
704     }
705 }